package crawler

import (
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"sync/atomic"
	"time"

	"github.com/gocolly/colly/v2"
	"github.com/gocolly/colly/v2/extensions"
	log "github.com/sirupsen/logrus"
	"github.com/twang2218/herbs/common"
)

type HerbCrawler struct {
	PageVisited int32
	PageTotal   int32

	cItem     *colly.Collector
	cIndex    *colly.Collector
	listeners []func(common.Herb)
}

const LINK_HERB string = "http://zhongyaocai360.com/zhongyao/"

func NewHerbCrawler() *HerbCrawler {
	hc := HerbCrawler{}
	//	创建基础爬虫
	u, err := url.Parse(LINK_HERB)
	if err != nil {
		log.Fatal(err)
	}

	hc.cItem = colly.NewCollector(
		colly.AllowedDomains(u.Hostname()),
		colly.DetectCharset(),
		colly.URLFilters(
			regexp.MustCompile(LINK_HERB),
		),
		colly.CacheDir(CRAWLER_CACHE_DIR),
		colly.Async(true),
	)
	hc.cItem.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: CRAWLER_PARALLELISM})
	hc.cItem.SetRequestTimeout(CRAWLER_REQUEST_TIMEOUT)
	extensions.RandomUserAgent(hc.cItem)
	hc.cItem.OnRequest(func(r *colly.Request) {
		r.ResponseCharacterEncoding = "utf-8"
		atomic.AddInt32(&hc.PageTotal, 1)
	})
	hc.cItem.OnResponse(func(r *colly.Response) {
		atomic.AddInt32(&hc.PageVisited, 1)
	})
	hc.cItem.OnError(func(resp *colly.Response, err error) {
		log.Warnf("HerbCrawler.OnError(): [Item] (%s) => '%s'", resp.Request.URL, err)
		//	重试。在另外线程等待一段时间，以不阻碍当前线程（爬虫）运行。
		go func(link string) {
			time.Sleep(CRAWLER_RETRY_TIMEOUT)
			hc.cItem.Visit(link)
		}(resp.Request.URL.String())
	})
	//	添加解析草药函数
	hc.cItem.OnHTML(".spider-2", hc.parseItem)

	//	创建索引爬虫
	hc.cIndex = hc.cItem.Clone()
	extensions.RandomUserAgent(hc.cIndex)
	hc.cIndex.OnRequest(func(r *colly.Request) {
		r.ResponseCharacterEncoding = "gb2312"
		log.Debugf("HerbCrawler => %s", r.URL)
	})
	hc.cIndex.OnError(func(resp *colly.Response, err error) {
		log.Warnf("HerbIndexCrawler.OnError(): [Index] (%s) => '%s'", resp.Request.URL, err)
		//	重试。在另外线程等待一段时间，以不阻碍当前线程（爬虫）运行。
		go func(link string) {
			time.Sleep(CRAWLER_RETRY_TIMEOUT)
			hc.cIndex.Visit(link)
		}(resp.Request.URL.String())
	})
	//	添加索引解析函数
	hc.cIndex.OnHTML(".mululi a", hc.parseIndex)

	hc.listeners = []func(common.Herb){}

	return &hc
}

func (c *HerbCrawler) Collect() {
	//	开始抓取
	c.cIndex.Visit(LINK_HERB)
	//	等待结束
	c.cIndex.Wait()
	c.cItem.Wait()
}

func (c *HerbCrawler) parseItem(e *colly.HTMLElement) {
	origins := e.ChildTexts(".article-h2")
	descs := []string{}
	e.ForEach(".article .spider", func(i int, h *colly.HTMLElement) {
		desc := strings.Join(h.ChildTexts("p"), "\n")
		descs = append(descs, desc)
	})

	h := common.Herb{
		Link:         e.Request.URL.String(),
		Descriptions: []common.HerbDescription{},
	}

	if len(origins) != len(descs) {
		log.Warnf("<出处和描述个数不符>：%d 个出处；%d 个内容。", len(origins), len(descs))
	}

	for i, origin := range origins {
		//	从 origin 中拆书名和药名
		oo := strings.Split(origin, "》")

		if len(oo) != 2 {
			log.Warnf("无法解析出处 '%s'", origin)
			continue
		}
		book := strings.Trim(oo[0], "《》 ")
		name := strings.TrimSpace(oo[1])

		if h.Name == "" {
			h.Name = name
		} else {
			if h.Name != name {
				log.Warnf("发现出处药名与原始药名不一致：'%s' => '%s'", h.Name, name)
			}
		}
		h.Descriptions = append(h.Descriptions, common.HerbDescription{
			Book:        book,
			Description: descs[i],
		})

		// minLength := int(math.Min(float64(len(descs[i])), 100.0))
		// log.Tracef("   %s: %s ...", origin, descs[i][:minLength])
	}

	log.Debugf("「%s」:  (%s)", h.Name, h.Link)
	var books []string
	for _, e := range h.Descriptions {
		books = append(books, fmt.Sprintf("《%s》", e.Book))
	}
	log.Tracef(" >  【%d 个出处】 %s", len(books), strings.Join(books, "、"))

	//	通知 OnHerbListeners
	c.notifyAllOnHerbListeners(h)
}

func (c *HerbCrawler) parseIndex(e *colly.HTMLElement) {
	link := strings.TrimSpace(e.Attr("href"))
	//	告知 cItem 抓取该链接
	c.cItem.Visit(link)
}

//	Listener functions

func (c *HerbCrawler) AddOnHerbListener(f func(common.Herb)) {
	if f == nil {
		log.Warn("HerbCrawler.AddOnHerbListener(): couldn't add 'nil' as listener.")
		return
	}

	c.listeners = append(c.listeners, f)
}

func (c *HerbCrawler) ClearOnHerbListener() {
	c.listeners = []func(common.Herb){}
}

func (c *HerbCrawler) notifyAllOnHerbListeners(h common.Herb) {
	for _, listener := range c.listeners {
		listener(h)
	}
}
